using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace Axial
{
    public class MethodBodyReader
    {
        public List<ILInstruction> Instructions;

        protected byte[] _il = null;
		private MethodBase _mi = null;

        private static OpCode[] _multiByteOpCodes;
		private static OpCode[] _singleByteOpCodes;

		static MethodBodyReader()
        {
            _singleByteOpCodes = new OpCode[0x100];
            _multiByteOpCodes = new OpCode[0x100];
            FieldInfo[] opCodeFields = typeof(OpCodes).GetFields();
            foreach (var num in Enumerable.Range(0, opCodeFields.Length))
            {
                FieldInfo fieldInfo = opCodeFields[num];
                if (fieldInfo.FieldType == typeof(OpCode))
                {
                    OpCode code = (OpCode)fieldInfo.GetValue(null);
                    ushort codeValue = (ushort)code.Value;
                    if (codeValue < 0x100)
                        _singleByteOpCodes[(int)codeValue] = code;
                    else
                        _multiByteOpCodes[codeValue & 0xff] = code;
                }
            }
        }

		#region il read methods
        private int ReadInt16(byte[] _il, ref int position)
        {
            return ((_il[position++] | (_il[position++] << 8)));
        }
        private ushort ReadUInt16(byte[] _il, ref int position)
        {
            return (ushort)((_il[position++] | (_il[position++] << 8)));
        }
        private int ReadInt32(byte[] _il, ref int position)
        {
            return (((_il[position++] | (_il[position++] << 8)) | (_il[position++] << 0x10)) | (_il[position++] << 0x18));
        }
        private ulong ReadInt64(byte[] _il, ref int position)
        {
            return (ulong)(((_il[position++] | (_il[position++] << 8)) | (_il[position++] << 0x10)) | (_il[position++] << 0x18) | (_il[position++] << 0x20) | (_il[position++] << 0x28) | (_il[position++] << 0x30) | (_il[position++] << 0x38));
        }
        private double ReadDouble(byte[] _il, ref int position)
        {
			double d = BitConverter.ToDouble(_il, position);
			position += 8;
			return d;
        }
        private sbyte ReadSByte(byte[] _il, ref int position)
        {
            return (sbyte)_il[position++];
        }
        private byte ReadByte(byte[] _il, ref int position)
        {
            return (byte)_il[position++];
        }
		private float ReadSingle(byte[] _il, ref int position)
        {
			float f = BitConverter.ToSingle(_il, position);
			position += 4;
			return f;
		}
        #endregion

        /// <summary>
        /// Constructs the array of ILInstructions according to the IL byte code.
        /// </summary>
        /// <param name="module"></param>
        private void ConstructInstructions(Module module)
        {
            byte[] il = _il;
            int position = 0;
            Instructions = new List<ILInstruction>();
            while (position < il.Length)
            {
                ILInstruction instruction = new ILInstruction();

                // get the operation code of the current instruction
                OpCode code = OpCodes.Nop;
                ushort value = il[position++];
                if (value != 0xfe)
                {
                    code = _singleByteOpCodes[(int)value];
                }
                else
                {
                    value = il[position++];
                    code = _multiByteOpCodes[(int)value];
                    value = (ushort)(value | 0xfe00);
                }
                instruction.Code = code;
                instruction.Offset = position - 1;
                int metadataToken = 0;
                // get the operand of the current operation
                switch (code.OperandType)
                {
                    case OperandType.InlineBrTarget:
                        metadataToken = ReadInt32(il, ref position);
                        metadataToken += position;
                        instruction.Operand = metadataToken;
                        break;
                    case OperandType.InlineField:
                        metadataToken = ReadInt32(il, ref position);
                        instruction.Operand = module.ResolveField(metadataToken);
                        break;
                    case OperandType.InlineMethod:
                        metadataToken = ReadInt32(il, ref position);
                        try
                        {
                            instruction.Operand = module.ResolveMethod(metadataToken);
                        }
                        catch
                        {
                            instruction.Operand = module.ResolveMember(metadataToken);
                        }
                        break;
                    case OperandType.InlineSig:
                        metadataToken = ReadInt32(il, ref position);
                        instruction.Operand = module.ResolveSignature(metadataToken);
                        break;
                    case OperandType.InlineTok:
                        metadataToken = ReadInt32(il, ref position);
                        try
                        {
                            instruction.Operand = module.ResolveType(metadataToken);
                        }
                        catch
                        {

                        }
                        break;
                    case OperandType.InlineType:
                        metadataToken = ReadInt32(il, ref position);
                        // now we call the ResolveType always using the generic attributes type in order
                        // to support decompilation of generic methods and classes

                        // thanks to the guys from code project who commented on this missing feature

                        instruction.Operand = module.ResolveType(metadataToken, _mi.DeclaringType.GetGenericArguments(), _mi.GetGenericArguments());
                        break;
                    case OperandType.InlineI:
                        instruction.Operand = ReadInt32(il, ref position);
                        break;
                    case OperandType.InlineI8:
                        instruction.Operand = ReadInt64(il, ref position);
                        break;
                    case OperandType.InlineNone:
                        instruction.Operand = null;
                        break;
                    case OperandType.InlineR:
                        instruction.Operand = ReadDouble(il, ref position);
                        break;
                    case OperandType.InlineString:
                        metadataToken = ReadInt32(il, ref position);
                        instruction.Operand = module.ResolveString(metadataToken);
                        break;
                    case OperandType.InlineSwitch:
                        int count = ReadInt32(il, ref position);
                        int[] casesAddresses = new int[count];
                        for (int i = 0; i < count; i++)
                            casesAddresses[i] = ReadInt32(il, ref position);
                        int[] cases = new int[count];
                        for (int i = 0; i < count; i++)
                            cases[i] = position + casesAddresses[i];
                        break;
                    case OperandType.InlineVar:
                        instruction.Operand = ReadUInt16(il, ref position);
                        break;
                    case OperandType.ShortInlineBrTarget:
                        instruction.Operand = ReadSByte(il, ref position) + position;
                        break;
                    case OperandType.ShortInlineI:
                        instruction.Operand = ReadSByte(il, ref position);
                        break;
                    case OperandType.ShortInlineR:
                        instruction.Operand = ReadSingle(il, ref position);
                        break;
                    case OperandType.ShortInlineVar:
                        instruction.Operand = ReadByte(il, ref position);
                        break;
                    default:
                        throw new Exception("Unknown operand type.");
                }
                Instructions.Add(instruction);
            }
        }

        public object GetReferencedOperand(Module module, int metadataToken)
        {
            AssemblyName[] assemblyNames = module.Assembly.GetReferencedAssemblies();
            foreach (int i in Enumerable.Range(0, assemblyNames.Length))
            {
                Module[] modules = Assembly.Load(assemblyNames[i]).GetModules();
                foreach (int j in Enumerable.Range(0, modules.Length))
                {
                    try
                    {
                        return modules[j].ResolveType(metadataToken);
                    }
                    catch
                    {
                    }

                }
            }
            return null;
        }

        /// <summary>
        /// Gets the IL code of the method
        /// </summary>
        /// <returns></returns>
        public string GetBodyCode()
        {
			return GetBodyCode(Instructions);
        }

		public static string GetBodyCode(IEnumerable<ILInstruction> instructions)
		{
			if (instructions == null)
				return "";

			string result = "";
			foreach (var inst in instructions)
				result += inst.GetCode() + "\n";
			return result;
		}

        /// <summary>
        /// MethodBodyReader constructor
        /// </summary>
        /// <param name="mi">
        /// The System.Reflection defined MethodInfo
        /// </param>
        public MethodBodyReader(MethodBase mi)
        {
            _mi = mi;
            if (mi.GetMethodBody() != null)
            {
                _il = mi.GetMethodBody().GetILAsByteArray();
                ConstructInstructions(mi.Module);
            }
        }
    }
}
